导读: 目前关系型数据库从上世纪70年代诞生以来得到了广泛应用,各种数字化的信息系统都能见到关系型数据库的身影。在真实的场景里面,业务系统对关系型数据库这种基础软件的要求非常简单,那就是高可靠和高性能,同时希望尽可能借助复杂的SQL语义来简化业务层功能的实现。传统数据库产品例如Oracle、SQLServer、MySQL、PostgreSQL等都发展趋于成熟,新一代的云原生数据库产品例如Aurora、PolarDB、TiDB、OceanBase等又开始引发更广泛的关注,那么什么样的数据库产品才能更好地适应业务发展?数据库这种比较古老的软件产品的未来又是什么?本文主要从商业产品系统的需求出发探讨数据库 技术的实践和思考。 一、商业产品系统对数据存储设施需求的特点 百度商业产品矩阵主要包括效果广告(搜索广告、信息流广告)和展示广告(品牌广告、开屏聚屏广告)两大类广告产品,以及基木鱼和观星盘等营销工具,商业产品系统是连接百度客户和广告检索系统的桥梁,帮助客户表达营销诉求,达成营销目标。 商业产品系统本质就是一个复杂、庞大的广告信息管理系统,有toB、toC的多种场景,需求多样丰富且迭代频繁。 投放,交易场景的事务型需求 (OLTP,On-Line Transaction Processing);
广告效果分析场景的分析型需求 (OLAP,Online analytical processing)
特定场景的高QPS查询 ,例如账户结构,权限关系等;
字面场景的正反KV查询 ,例如关键词字面和id互查等;
物料列表场景的模糊查询;
为了应对商业场景下如此多样且迥异的数据存储需求,如果使用传统的存储技术,至少需要使用关系型数据库(例如MySQL)、KV存储(例如Redis)、OLAP 数仓(例如Palo)、全文检索(例如ElasticSearch)以及自定义内存结构的存储等。 那么业务系统对数据存储设施的要求是什么呢?
首先是稳定可靠 ,不可用就意味着客户体验受损乃至直接的经济损失; 其次数据尽可能一致 ,如果客户在不同环节看的数据有差异则会产生误解甚至引发错误的广告投放操作; 再次尽可能低成本 的应对数据规模持续增长,不需要预先购置大量硬件,后期扩展时也尽可能简单; 最后综合读写性能好 ,尽量毫秒级响应,不影响客户的操作体验。
对于业务研发的同学来说,他们希望用到的数据存储产品是什么样呢?
接口的使用方式单一,学习和迁移成本低,不同的数据存储也尽量采用相同的接口形式; 数据变更行为可理解,不出现数据丢失或者覆盖,不因并发引入异常数据; 扩展性高,能够适应数据规模和流量从1到N的变化,业务最好无感知; 高可用,内建高度容错能力,业务对数据库异常最好无感知; 总结起来最好什么都可以干,什么负载都可以扛,什么运维都不用管! 二、BaikalDB 的发展历程 商业系统最核心的存储需求就是广告库,广告库存储了所有的广告物料信息,用于完成整个广告生命周期的管理,帮助客户完成全部广告投放功能,获取转化。伴随百度凤巢系统的发展,广告库的存储设施经历了两个重要的阶段: 1. 单库到分库分表的MySQL集群 2. MySQL主存储集群+镜像辅助存储构成的异构复合存储集群 2.1 分库分表的 MySQL 集群 最早的凤巢广告库采用单机MySQL,部署在独立的盘柜(高性能磁盘阵列)上,这种架构受限于当时的硬件条件现在看来比较古老,但这个跟现在流行的存储计算分离的云原生架构从思想上是完全一致的,AWS的Aurora或者阿里云的PolarDB就是把MySQL、PostgreSQL等单机数据库部署到一个由EBS磁盘或者RDMA高速网络连接的分布式文件系统上,实现100%的SQL兼容。 随着业务发展,单机部署的MySQL无法支撑数据量和读写量的膨胀,分库分表就成了当时乃至现在最优的选择,通过分库分表,MySQL可以实现容量和性能的高扩展性。
从2010年开始,凤巢广告库就依次经历了1拆4、4拆8、8拆16、16拆32的分库过程,从一套单机集群发展成了有33分库(多拆出来的一个分库是为了解决个别大客户购买巨量关键词的场景),每分库1主11从的多分库集群,存储了数十TB的广告物料信息,读写PV达到每日数十亿。拆库的服务停顿时间从一天到6个小时,再到分钟级别。 2.2 异构复合存储集群
凤巢广告库的业务场景是读多写少,查询场景多样,多分库MySQL集群在满足一些查询场景较为吃力,比如在账户-计划-单元-关键词层级结构里,获取账户下关键词数,计划下的关键词数等涉及全表扫的count,关键词字面高qps查询,创意模糊搜索,物料列表分筛排等,这些需求使用MySQL都难以满足。 为解决这个问题,我们通过数据流,把MySQL的数据实时同步到一个镜像的内存存储,这些镜像存储采用针对特定查询场景的内存结构,来满足业务性能。同时为了业务应用的开发便利,还专门开发了SQL代理层,按照一定规则在SQL不改变的情况路由到镜像索引,并转化为镜像存储所需要的请求参数,这样虽然我们使用了不同的数据源,但是业务应用仍然认为是一个 MySQL协议的数据库在提供服务,且无需要关注应该查询哪种数据源,由此形成一个异构的复合存储形态。架构如下图所示: 这是一种常见的架构设计,在另一些业务场景中会把OLTP数据库的数据同步到OLAP数据仓库,隔离离线分析场景,它的优势在于多套同种数据不同存储引擎的系统通过分而治之来解决复杂的查询场景,并具有一定业务隔离性。 依靠SQL代理层能够有效提升业务应用的使用体验,并且可以把应用层分库分表逻辑也下沉到这个代理层,拆库时业务应用也无需感知。对于业务应用来说,看到的是一个单机的MySQL系统,不再需要考虑任何性能和容量的问题。 但是这种架构也有明显的缺点:
运维更为复杂: 除了关注 MySQL 本身,还需要运维数据实时同步流,SQL代理层,镜像索引这些系统。
数据实时同步容易出现故障或者延迟: 客户可能感知到明显的不一致,从镜像索引查询到的数据跟从MySQL查询有差异。为了降低这种差异的影响,SQL代理层还需要设计一定的降级能力(发现延迟时尽可能切换到MySQL查询)。还需要有快速修正镜像索引数据的设施。
资源冗余浪费: 镜像索引实际是数据的复制, MySQL为扛住读性能和同步需求需要大量的从库。
2.3 2017年的选择
时间来到2017年,凤巢广告库已经有33分库,磁盘也用上了NVME SSD,对于限定场景的读写性能可以满足业务需求,但是如果再进行一次拆库,无论是资源消耗还是运维成本都更为巨大。 到这个阶段,我们开始思考是否存在一种成本更低的解决方案。新的信息流广告业务也在快速发展,如果再形成一套凤巢广告类似的存储架构,实际成本会非常可观。虽然4年后的今天,凤巢广告库依靠硬件升级,包括CPU和内存升级、NVME SSD升级到单盘3T,依然维持在33分库的部署架构,但性能瓶颈已经开始突显,如果广告物料继续高速增长,预计2022年底就需要进行新的拆库。当时广告系统的业界标杆Google AdWords的核心存储是F1/Spanner,采用全球部署可以跨远距离的数据中心多活,配备原子钟用于实现分布式强一致事务,具备极高的可用性和自动增容的扩展性。参考Google存储系统的设计理念,广告存储系统设计也有可见的两种路线: 2.3.1 基于MySQL深度定制
MySQL是一种单机的架构,代码规模达到百万行级别,掌控和修改的难度都特别高。如果要把MySQL从内部改造成一种类似F1/Spanner能力的系统基本不大可能。 类似Aurora和PolarDB,在文件系统上进行突破,使用EBS或者构建一种RDMA高速连接的分布式文件系统,这并不是研发新的数据库系统。但是为获取更好的性能,依然需要深入 MySQL的存储引擎和主从同步机制进行一些定制和深度优化。即便如此,总容量和性能也不能无限扩展,例如Aurora最高可达128TB,性能是MySQL的5倍,PolarDB最高可达100TB,性能是MySQL的6倍。
类似凤巢广告存储的设计思路,通过数据同步并借助扩展的镜像索引提升查询性能,但冗余成本高,数据一致性差。
2.3.2 使用满足分布式+云原生+多样化索引架构+强一致等条件的新数据库系统 2017年的时候,无论是Google的F1/Spanner还是OceanBase都是闭源系统,跟内部设施耦合很大。开源系统主要有两个流派,一类是支持SQL的OLAP系统,例如百度的Palo(现开源名为Doris)、Impala(无存储引擎)、ClickHouse等,一类是参考F1/Spanner思想的CockroachDB和TiDB。OLAP系统肯定不太满足我们TP(在线事务)场景的主需求,当时CockroachDB和 TiDB也处于起步阶段,生产场景的使用基本没有。 这时放眼望去,实际并没有特别成熟的解决方案,基于MySQL的方案也走到了一个瓶颈,那么我们能否自研一个新的分布式数据库系统?当时的决策依据是看团队是否具备能力从零研发出一个高可用、高性能、低成本的OLTP为主兼顾OLAP的数据库(也就是HTAP,Hybrid Transaction and Analytical Process,混合事务和分析处理)。
团队的条件: 已有的存储方向团队(4人)是C++技术栈,研发过SQL代理层和定制化存储,熟悉MySQL协议,有实战的工程经验。
技术的条件:
1、分布式系统需要有效的通信框架,百度的brpc框架当时已经非常成熟,是工业级的RPC实现,有超大规模的应用。
2、保障数据一致性当时主流的方案就是Paxos和Raft,百度braft框架是基于brpc的Raft协议实现,发展也很迅速,有内部支持。
3、单机存储节点需要一个可靠的KV存储,Facebook&Google联合出品的RocksDB是基于LSM-Tree的高性能KV引擎,CockroachDB和TiDB都选择了RocksDB。
后来经过8个月的设计研发,我们的1.0版本数据库就完成上线,结果也证明了我们的决策是可行的。 2.4 面向商业产品系统的新一代存储系统BaikalDB BaikalDB是面向商业产品系统的需求而设计的分布式数据库系统,核心的目标有三个: 1、灵活的云上部署模式: 面向容器化环境设计,能够与业务应用混部,灵活迁移,容量和性能支持线性扩展,成本低廉,不需要特殊硬件。 2、一站式存储计算能力: 面向业务复杂需求具备综合的适应性,主要满足OLTP需求,兼顾OLAP需求、全文索引需求、高性能KV需求等。 3、兼容MySQL协议: 易于业务使用,学习成本低。 BaikalDB命名来自于Lake Baikal(世界上容量最大的淡水湖-贝加尔湖),贝加尔湖是世界上容量最大的淡水湖,相当于北美洲五大湖水量的总和,超过整个波罗的海水量,淡水储量占全球20%以上。西伯利亚总共有336条河流注入贝加尔湖。冬天的贝加尔湖畔,淡蓝色的冰柱犹如分布式数据库一列列的数据密密麻麻但是有井然有序,令人惊艳。 BaikalDB是一个兼容MySQL协议的分布式可扩展存储系统,支持PB级结构化数据的随机实时读写,整体系统架构如下: BaikalDB基于RocksDB实现单机存储,基于Multi Raft协议(braft库)保障副本数据一致性,基于brpc实现节点通讯交互,其中 BaikalStore 负责数据存储,用Region组织,三个Store的三个Region形成一个Raft group实现三副本,多实例部署。Store实例宕机可以自动迁移Region数据。
BaikalMeta 负责元信息管理,包括分区、容量、权限、均衡等, Raft保障的3副本部署,Meta宕机只影响数据无法扩容迁移,不影响数据读写。
Baikaldb 负责前端SQL解析,查询计划生成执行,无状态全同构多实例部署,宕机实例数不超过QPS承载极限即可。
全自主化的容量管理: 可以自动扩容和自动数据均衡,应用无感知,很容易实现云化,目前运行在Opera PaaS平台之上
高可用,无单点: 支持自动故障恢复和迁移
面向查询优化: 支持各种二级索引,包括全文索引,支持多表join,支持常见的OLAP需求
兼容MySQL协议,支持分布式事务: 对应用方提供SQL界面,支持高性能的Schema和索引变更
支持多租户: meta信息共享,数据存储完全隔离
在系统研发过程中,BaikalDB以业务需求为导向规划快速迭代,在业务使用中深度打磨优化,随业务成长而成长,关键功能的时间节点如下: 从2018年上线以来,BaikalDB已部署1.5K+数据表,数据规模达到600+TB,存储节点达到1.7K+。 三、BaikalDB 关键设计的思考和实践
分布式数据存储系统一般有三种架构模式,分别是Shared Everthing、Shared Disk和Shared Nothing。 1、Shared Everthing: 一般是针对单个主机,完全透明共享CPU、内存和磁盘,传统RDMS产品都是这种架构。 2、Shared Disk: 各个处理单元使用自己的私有CPU和内存,但共享磁盘系统,这可以实现存储和计算分离,典型的代表是Oracle Rac(使用SAN共享数据)、Aurora(使用EBS)、PolarDB(使用RDMA)。 3、Shared Nothing: 各个处理单元都有自己私有的CPU、内存和磁盘等,不存在资源共享,类似于MPP(大规模并行处理)模式,各处理单元之间可以互相通信,并行处理和扩展能力更好。典型代表是hadoop,各node相互独立,分别处理自己的数据,处理后可能向上层汇总或在节点间流转。 Shared Disk是很多云厂商倡导的架构,云厂商希望在云上提供一个完全兼容传统RDMS系统的云产品,希望广大的数据库使用者基本没有迁移成本,但是各家云厂商的实现有比较多差异,主要比拼性能、容量和可靠性,这些也是各家云厂商吸引客户的卖点。但是该架构的Scale Out(横向扩容)能力比较有限,所以云厂商的存储广告宣传语一般是百TB级别的数据容量。 Shared Nothing是一种分布式的依靠多节点来工作的架构,大部分NoSQL都是这样的架构,Sharding MySQL集群也是一种Shared Nothing的架构,每个分片独立工作。这类架构最大的局限是难以同时保障一致性和可用性,也就是受限于著名的CAP理论。对于NoSQL系统大部分不支持事务,所以优先保障可用性。但对于OLTP场景,数据一致性非常重要,事务是不可或缺的环节。 因为BaikalDB的目标是一个面向业务需求的具备融合型能力的分布式数据存储,并且大规模数据场景更看重Scale Out能力(仅仅100TB 容量远远不够),所以采用的是Shared Nothing的架构。 对于分布式数据系统而言,设计最需要关注存储、计算、调度三个方面的内容。 3.1 存储层的设计
存储层的设计主要是关注用什么样的数据结构来描述数据存放。对于分布式数据系统,还需要额外关注怎么利用多节点来协同存储同一份数据。 对于大规模数据场景的存储优先需要考虑使用磁盘,而不是成本更高数据易失的内存。面向磁盘的存储引擎,RocksDB是比较突出的代表,其核心模型是Key-Value结构。如果使用RocksDB就需要考虑如何数据表的结构映射到Key-Value结构。 为了把数据分散到多台机器,BaikalDB还引入了Region的概念,用于描述最小的数据管理单元,每个数据表是有若干个Region构成,分布到多台机器上。这样的架构就需要考虑如何对数据进行拆分,一般有Hash(根据Key的Hash值选择对应的机器)和Range(某一段连续的Key都保存在一个机器上)两种。 Hash的问题在于如果Region增大到需要分裂时如何动态修改Hash规则,而Hash规则的改动会涉及大量数据的重新分布,每个Region的大小都很难均衡,即使引入一致性Hash也只能有限改善该问题。Range虽然容易实现数据的分裂拆分,但是容易有热点,不过相对来说好克服。所以BaikalDB采用了Range拆分。 Key-Value不等同于数据库的Table,需要把数据表的主键索引(也叫聚簇索引,存储主键值和全部数据),和面向查询优化的二级索引(也叫非主键索引、非聚簇索引,存储索引值和主键值)以及全文索引都要映射到Key-Value模型里面。 二级索引在Key-Value模型里面,不管本地的还是全局的,Key都是由region_id、index_id、索引键值、主键值(如果是二级唯一索引就无需包含),Value是主键值,可以看出使用二级索引拿到整行数据还需要从主键索引再获取一次(也就是回表操作),如果相关数据都在索引键值里就不需要回表。 BaikalDB在早期没有引入分布式事务(实在太复杂),所以先实现了本地二级索引,在实现分布式事务后再实现了全局二级索引。对于业务应用而言,可以按使用场景优先选择本地二级索引。 构建: 将正排字段切词为一或多个term。构建term的有序倒排拉链,并按照格式进行存储。 在Key-Value模型里面,Key为region_id、index_id、分词后的 term,Value为排序好的主键键值。 所以在存储层面,包括以上主要核心逻辑结构,以及列存、HLL、TDdigest等都是KV的物理结构。关于更多的索引细节设计请参考 BaikalDB 的索引实现(https://my.oschina.net/BaikalDB/blog/4514979)。 数据基于Range方式按照主键切分成多个分片(Region)后,同时为提升在分布式场景下的整体可用性,需要多个副本(Replica)来存储同样的Region,这时就需要考虑多个副本和多个分片的数据一致性:
3.2 计算层的设计
计算层需要关注如何把SQL解析成具体的查询计划,或者叫分布式的计算过程,还需要考虑如何基于代价进行优化。 BaikalDB的SQL层面是一种分布式的分层设计,整体架构如下:目前BaikalDB还不是完全的MPP架构,跟OLAP系统的设计有较大差异,数据最后的计算汇总只发生在一个Baikaldb节点上,同时各种Filter条件会尽量下推到BaikalStore模块,减少最后需要汇总的数据,考虑到OLTP场景为主的情况下返回数据规模有限,所以也足够使用。因此BaikalDB存储节点具备一定的计算能力,可以分布式执行,降低传输压力,所以也不是严格的存储和计算分离。 在SQL引擎的实现层面,我们采用了经典的火山模型,一切皆算子,包括Baikaldb和BaikalStore的交互也是算子,每个算子提供open、next、close操作,算子之间可以灵活拼接,具备很好的扩展性。 在查询条件执行过程中,如果数据表有多种索引,为了让查询更优,还需要具备 自动选择最合适索引的能力,这种查询优化器的主流设计有两种: 基于规则的优化器(RBO,Rule-Based Optimization): 该方式按照硬编码在数据库中的一系列规则来决定SQL的执行计划。实际过程中,数据的量级差异会影响相同SQL的性能,这也是RBO的缺陷所在,规则是不变的,数据是变化的,最后规则决定的不一定最优。
基于代价的优化器(CBO,Cost-Based Optimization): 该方向通过根据优化规则会生成多个执行计划,然后CBO会通过根据统计信息(Statistics)和代价模型(Cost Model)计算各种执行计划的代价,即COST,从中选用COST最低的执行计划作为实际执行计划。统计信息的准确与否会影响CBO做出最优的选择。
查询优化器也是非常复杂的话题,现在还有基于AI技术的查询优化器,也是学术研究的热门,在很多数据库研发公司,这一般也是一个专门的大方向。BaikalDB采用了RBO和CBO结合的方式做查询优化,关于CBO的细节可以参考 BaikalDB 的代价模型实现(https://my.oschina.net/BaikalDB/blog/4715063)。
3.3 调度层的设计
分布式数据系统涉及到多个工作节点,每个节点可能有不用的硬件环境和软件负载,为尽可能发挥集群的性能,肯定希望每个工作节点存储的数据大小基本一致,数据处理的负载基本一致,但同时还需要考虑故障节点的避让和恢复,保持集群的性能平稳。 调度系统基本都会有一个Master角色来综合评估集群所有节点的信息做出调度决策,BaikalDB的Master角色是BaikalMeta模块,BaikalStore定时通过心跳包收集信息上报给BaikalMeta,BaikalMeta获得整个集群的详细数据后根据这些信息以及调度策略来生成决策,这些决策会通过心跳包的回复发送给BaikalStore,BaikalStore会根据实际情况来灵活执行,这里并不需要保证操作的执行成功,后续还会通过心跳告知BaikalMeta执行情况。 BaikalMeta每次决策只需要根据本轮收集的所有节点的心跳包的结果来处理,不需要依赖之前的心跳包信息,这样决策逻辑就比较容易实现。如果BaikalMeta故障,BaikalStore的心跳包没有回应,就会停止全部的调度操作,整个集群处于不调整的状态,同时Baikaldb模块会缓存BaikalMeta返回的集群信息,能准确知道每个数据表的全部Region信息,并能对失败的节点做剔除或者重试,这样即使BaikalMeta故障,也不会影响读写。 另一方面BaikalMeta的决策并不需要很高的时效性,所有BaikalStore可以间隔较长时间发送心跳,有效控制对BaikalMeta请求压力,这样一组BaikalMeta就可以管理成千上万的BaikalStore节点。 在存储节点的调度中主要需要关注Leader的均衡和Peer的均衡: Leader 均衡 : 每个BaikalStore节点的Region Leader数量应尽量一致,Leader是主要的读写压力承担者,均衡可以让每个BaikalStore节点的 CPU 内存负载接近。在BaikalStore负载较高时(通常容器化环境下会显著变高),如果同机器的其他容器消耗大量的CPU和内存,同一个BaikalStore的其他Leader也可能消耗大量资源,就需要把它上面的Leader切换到其他节点,避免热点导致的处理超时。
Peer 均衡: 指每个Raft Group的副本尽量分散到每个BaikalStore节点,使得每个BaikalStore节点的副本数量尽可能一致,每个数据表的所有Region大小基本是一致的,因此使得每个BaikalStore的存储容量也比较接近,避免数据倾斜,这样能够重复利用集群的磁盘资源。此外还希望每个副本在不同的机器,甚至不同的网段上,避免机器故障和网络故障导致一个Region的大部分副本不可用进而导致 Leader 无法选出不能读写。在Peer有节点故障或者主动迁移时,还需要创建新的Peer同步数据达成可用,并删除不可用Peer,这样来保障Peer数量稳定。
Region作为一个调度单元,它的可分裂性也是调度机制的一个基础,BaikalDB 会在 Region大小超过设定阈值时,采用基准+增量的方式来拆分Range产生新的Region及其副本,通过汇报信息进行调度均衡,这样使得在数据增长的时候可以自动化拆分。调度也是一个比较复杂的话题,通过引入很多调度策略能够提升资源的利用率、容灾、避免热点,保障性能,这块工作也是BaikalDB迭代的重点方向。 四、总结
本文从大规模商业系统的需求出发,总结了商业场景对数据存储设施的期望。通过回顾整个凤巢广告库依赖的数据库系统的发展过程,来展示了商业平台研发部自研更低成本、更为可靠、更为强大的数据存储系统-BaikalDB的迭代历程。 经过4年的工作,BaikalDB已经整合了商业产品系统历史存在的全部存储系统,实现了大一统。在结合业务需求研发过程中,BaikalDB也尽可能依靠很少的人力投入,快速构建核心功能集,根据业务需求的紧迫程度逐渐迭代,不仅仅满足了广告场景的需求,还满足新的包括落地页和电商的新商业场景的需求,而且仍在不停的丰富功能、优化性能和降低成本,打磨整个系统。 最后针对如何研发一个数据库,从存储、计算、调度三个角度总结了BaikalDB的一些关键设计思路。数据库和操作系统、编译器并称三大系统软件,可以说是整个计算机软件的基础设施,数据库技术同样是博大精深,本文只是以业务视角管中窥豹,难免有疏漏,希望大家探讨指正。 最后希望大家多多关注 BaikalDB的开源项目github.com/baidu/BaikalDB 。 招聘信息
百度商业平台研发部主要负责百度商业产品的平台建设,包括广告投放、落地页托管、全域数据洞察等核心业务方向,致力于用平台化的技术服务让客户及生态伙伴持续成长,成为客户最为依赖的商业服务平台。
无论你是后端,前端,还是算法,这里有若干职位在等你,欢迎投递简历, 百度商业平台研发部期待你的加入!
简历投递邮箱:geektalk@baidu.com (投递备注 【百度商业】)
技术原创及架构实践文章,欢迎通过公众号菜单「联系我们」进行投稿。